﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PSE.Updater.Common.Gateway.DataContract;
using PSE.Updater.Common.Providers;
using PSE.Updater.Common.Configurations.Client;
using System.Configuration;
using PSE.Updater.Common;
using System.ServiceModel;
using PSE.Updater.Common.Gateway;
using PSE.Updater.Common.Gateway.ServiceContract;
using System.IO;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Xml;
using PSE.Framework.ErrorLogging;
using System.Reflection;

namespace PSE.Updater.Client.Service
{
    public class Proxy
    {
        /// <summary>
        /// Evendo disparado ao ser efetuado o Download
        /// </summary>
        /// <param name="sender">Objeto de Origem</param>
        /// <param name="e">Informações sobre o download</param>
        public delegate void DownloadProgressEventHandler(object sender, DownloadProgressEventArgs e);
        /// <summary>
        /// Evendo disparado ao ser efetuado o Download
        /// </summary>
        public event DownloadProgressEventHandler DownloadProgressChanged;

        private UpdateConfigurationSection _configuration = null;

        #region [ Singleton ]
        public Proxy()
        {
            _configuration = UpdateConfiguration.GetInformation();

            if (_configuration == null)
                throw new ConfigurationErrorsException("The configuration section 'UpdateConfiguration' is invalid or is missing");
        }
        private static object _singletonLock = new object();
        private static Proxy _proxy;
        public static Proxy Instance
        {
            get
            {
                lock (_singletonLock)
                {
                    if (_proxy == null)
                        _proxy = new Proxy();

                    return _proxy;
                }

            }
        }
        #endregion [ Singleton ]

        private string GetDownloadPackagePath()
        {
            string assemblyPath = Assembly.GetEntryAssembly().Location;
            string folder = System.IO.Path.GetDirectoryName(assemblyPath);
            string downloadPath = System.IO.Path.Combine(folder, "Downloads");

            try
            {
                if (!System.IO.Directory.Exists(downloadPath))
                    System.IO.Directory.CreateDirectory(downloadPath);
            }
            catch (Exception ex)
            {
                ErrorLogger.WriteLog(LogType.Error, ex);
                throw;
            }

            return downloadPath;
        }

        /// <summary>
        /// Recuperar a lista de pacotes de atualização disponíveis para os módulos informados
        /// </summary>
        /// <param name="modules">Lista de módulos dos quais recuperar atualizações</param>
        /// <returns>Lista de atualizações disponíveis para os módulos</returns>
        public List<ModuleUpdatePackage> GetAvailableUpdates(ProviderType type, List<ModuleVersion> modules)
        {
            if (modules == null)
                return null;

            List<ModuleUpdatePackage> moduleUpdatePackages = null;

            string downloadPath = GetDownloadPackagePath();
            try
            {
                if (System.IO.Directory.Exists(downloadPath))
                    System.IO.Directory.Delete(downloadPath, true);
            }
            catch (Exception ex)
            {
                ErrorLogger.WriteLog(LogType.Error, ex);
                throw;
            }

            Connect<IUpdatesDiscoveryService>("UpdatesDiscoveryService", TransferMode.Buffered,
                (service) =>
                {
                    moduleUpdatePackages = service.GetAvailableUpdates(type, modules);
                });

            return moduleUpdatePackages;
        }

        /// <summary>
        /// Efetua o Download do Arquivo de Atualização
        /// </summary>
        /// <param name="moduleUpdatePackage">Informações do Módulo</param>
        /// <returns>Caminho do Arquivo</returns>
        public string DownloadUpdatePackage(ProviderType type, ModuleUpdatePackage moduleUpdatePackage)
        {
            string path = string.Empty;

            if (moduleUpdatePackage.Status == PackageStatus.Missing)
                return string.Empty;

            string downloadPath = GetDownloadPackagePath();
            if (!string.IsNullOrEmpty(downloadPath))
            {

                Connect<IUpdatesDownloadService>("UpdatesDownloadService", TransferMode.Streamed,
                    (service) =>
                    {
                        Stream packageFile = service.DownloadUpdatePackage(type, moduleUpdatePackage);
                        path = SaveFile(packageFile, moduleUpdatePackage, downloadPath);
                    });
            }

            return path;
        }

        /// <summary>
        /// Registra um produto para que ele seja atualizado pelo Updater
        /// </summary>
        /// <param name="providerType">Tipo de Atualização</param>
        /// <param name="productName">Nome do Produto</param>
        /// <param name="folder">Pasta para monitoração</param>
        public void RegistryProduct(ProviderType providerType, string productName, string folder)
        {
            Connect<IUpdatesRegistryProductFolders>("UpdatesRegistryProductFoldersService", TransferMode.Streamed,
                (service) =>
                {
                    service.AddFolder(providerType, productName, folder);
                });
        }

        /// <summary>
        /// Remove o produto para não ser mais atualizado pelo Updater
        /// </summary>
        /// <param name="productName">Nome do Produto</param>
        public void UnregistryProduct(string productName)
        {
            Connect<IUpdatesRegistryProductFolders>("UpdatesRegistryProductFoldersService", TransferMode.Streamed,
                (service) =>
                {
                    service.RemoveAllFolders(ProviderType.Updates, productName);
                    service.RemoveAllFolders(ProviderType.ConfigurationFile, productName);
                });
        }

        /// <summary>
        /// Salva o Arquivo em disco
        /// </summary>
        /// <param name="packageFile">Pacote</param>
        /// <param name="moduleUpdatePackage">Informações sobre o Arquivo</param>
        /// <param name="downloadPath">Caminho para download</param>
        /// <returns>caminho do Arquivo</returns>
        private string SaveFile(Stream packageFile, ModuleUpdatePackage moduleUpdatePackage, string downloadPath)
        {
            string filePath = string.Empty;

            if (packageFile != Stream.Null)
            {
                byte[] buffer;
                int count, readed, percent, lastPercent;

                //se o arquivo ja existir, apagar
                FileInfo file = new FileInfo(moduleUpdatePackage.FileName);
                filePath = Path.Combine(downloadPath, file.Name);
                if (File.Exists(filePath))
                    File.Delete(filePath);

                //ler o stream e salvar o arquivo em disco
                using (FileStream fs = new FileStream(filePath, FileMode.CreateNew))
                {
                    readed = 0;
                    count = 20480;
                    lastPercent = 0;
                    percent = 0;

                    buffer = new byte[count];
                    while ((readed = packageFile.Read(buffer, 0, count)) > 0)
                    {
                        fs.Write(buffer, 0, readed);
                        buffer = new byte[count];
                        if (this.DownloadProgressChanged != null)
                        {
                            percent = (int)((fs.Length * 100) / moduleUpdatePackage.PackageSize);
                            if (percent != lastPercent)
                            {
                                lastPercent = percent;
                                try
                                {
                                    DownloadProgressEventArgs e = new DownloadProgressEventArgs(percent);
                                    this.DownloadProgressChanged(this, e);
                                    if (e.Cancel)
                                    {
                                        fs.Dispose();
                                        if (File.Exists(filePath))
                                            File.Delete(filePath);

                                        filePath = string.Empty;
                                        break;
                                    }
                                }
                                catch (Exception ex)
                                {
                                    //apenas logar para evitar que erros em codigo externo impacte no funcionamento do componente
                                    ErrorLogger.WriteLog(LogType.Error, ex);
                                }
                            }
                        }
                    }
                }

                packageFile.Close();
            }
            if (!File.Exists(filePath))
            {
                //ErrorLogger.WriteLog(LogType.Error, new FileNotFoundException(filePath));
                return string.Empty;
            }
            else
            {
                FileInfo fileInfo = new FileInfo(filePath);
                if (fileInfo.Length == 0)
                {
                    Exception ex = new Exception(string.Format("Arquivo {0} corrompido ou inválido", filePath));
                    //ErrorLogger.WriteLog(LogType.Error, ex);
                    return string.Empty;
                }
            }
            return filePath;
        }

        /// <summary>
        /// Método genérico para conexão com um serviço
        /// </summary>
        /// <typeparam name="TChannel">Canal para conexão</typeparam>
        /// <param name="serviceName">Nome do Serviço</param>
        /// <param name="tm">Tipo de transferência</param>
        /// <param name="callMethod">Método acionado quando for efetuada a conexão com o serviço</param>
        private void Connect<TChannel>(string serviceName, TransferMode tm, Action<TChannel> callMethod)
        {
            Exception exeption = null;

            ChannelFactory<TChannel> serviceChannel = null;

            List<string> servers = new List<string>();
            string serverDefault = System.IO.Path.Combine(_configuration.DefaultUpdateServerUrl, serviceName);
            servers.Add(serverDefault);
            foreach (UpdateServersConfigurationElement server in _configuration.UpdateServers)
                servers.Add(System.IO.Path.Combine(server.Url, serviceName));

            NetTcpBinding bindings = BindingFactory.CreateNetTcpBinding(tm);

            foreach (var server in servers)
            {
                try
                {
                    EndpointAddress endpointAddress = new EndpointAddress(new Uri(server));
                    serviceChannel = new ChannelFactory<TChannel>(bindings, endpointAddress);

                    foreach (OperationDescription op in serviceChannel.Endpoint.Contract.Operations)
                    {
                        var dcSerializer = op.Behaviors.Find<DataContractSerializerOperationBehavior>();
                        if (dcSerializer != null)
                            dcSerializer.MaxItemsInObjectGraph = Int32.MaxValue;
                    }

                    serviceChannel.Open();

                    TChannel client = serviceChannel.CreateChannel();
                    callMethod(client);

                    exeption = null;

                    break;
                }
                catch (EndpointNotFoundException ex)
                {
                    ErrorLogger.WriteLog(LogType.Error, ex);
                    continue;
                }
                catch (CommunicationException ex)
                {
                    exeption = ex;
                    break;
                }
                catch (Exception ex)
                {
                    exeption = ex;
                    break;
                }
                finally
                {
                    try
                    {
                        if (serviceChannel != null && serviceChannel.State != CommunicationState.Faulted)
                            serviceChannel.Close();
                    }
                    catch
                    {
                        serviceChannel.Abort();
                    }
                }
            }

            if (exeption != null)
                throw exeption;

        }
    }
}
